/*
 * Code Pulse: A real-time code coverage testing tool. For more information
 * see http://code-pulse.com
 *
 * Copyright (C) 2014 Applied Visions - http://securedecisions.avi.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.secdec.codepulse.tracer.export

import java.io.BufferedOutputStream
import java.io.OutputStream
import java.util.Properties
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream

import com.secdec.codepulse.data.model._

import net.liftweb.http.OutputStreamResponse

/** Responsible for exporting trace projects to a portable format. In the current
  * incarnation, it exports to a .pulse file (which is really just a zip file)
  * containing a .version manifest file and a JSON representation of the data.
  *
  * Output is to an OutputStream. It is up to the caller to pipe this to the
  * appropriate destination. A convenience wrapper is provided to provide an
  * `OutputStreamResponse`.
  *
  * @author robertf
  */
object ProjectExporter extends JsonHelpers {
	val Version = 2.1

	def zipEntryInputFilePrefix = "input-file_"

	/** Export `data` to `export` */
	def exportTo(out: OutputStream, data: ProjectData) {
		val bos = new BufferedOutputStream(out)
		val zout = new ZipOutputStream(bos)

		try {
			write(zout, ".manifest") { writeManifest(_) }
			write(zout, "project.json") { writeMetadata(_, data.metadata) }
			write(zout, "sourceFiles.json") { writeSourceFiles(_, data.sourceData) }
			write(zout, "nodes.json") { writeTreeNodeData(_, data.treeNodeData) }
			write(zout, "method-mappings.json") { writeMethodMappings(_, data.treeNodeData) }
			write(zout, "jsp-mappings.json") { writeJspMappings(_, data.treeNodeData) }
			write(zout, "recordings.json") { writeRecordings(_, data.recordings) }
			write(zout, "sourceLocations.json") { writeSourceLocations(_, data.sourceData) }
			write(zout, "encounters.json") { writeEncounters(_, data.recordings, data.encounters) }

			val inputFilePath = data.metadata.input
			val inputFilePathMissing = inputFilePath == null || inputFilePath.trim().isEmpty()
			if (inputFilePathMissing) return

			val inputFileName = new java.io.File(inputFilePath).getName()
			write(zout, zipEntryInputFilePrefix + inputFileName) { writeInputFile(_, data.metadata.input) }
		} finally {
			zout.finish
			bos.flush
		}
	}

	/** Export `data`, providing an `OutputStreamResponse`. */
	def exportResponse(data: ProjectData) = {
		val headers = List(
			"Content-Type" -> "application/zip",
			"Content-Disposition" -> s"""attachment; filename="${data.metadata.name}.pulse"""")

		OutputStreamResponse(exportTo(_, data), -1L, headers, Nil, 200)
	}

	private def write[T](zout: ZipOutputStream, name: String)(f: OutputStream => T) = {
		zout putNextEntry new ZipEntry(name)

		try {
			f(zout)
		} finally zout.closeEntry
	}

	private def writeManifest(out: OutputStream) {
		val props = new Properties
		props.setProperty("version", Version.toString)
		props.store(out, "Export generated by Code Pulse")
	}

	private def writeMetadata(out: OutputStream, metadata: ProjectMetadataAccess) {
		streamJson(out) { jg =>
			jg.writeStartObject

			jg.writeStringField("name", metadata.name)
			jg.writeNumberField("creationDate", metadata.creationDate)

			jg.writeEndObject
		}
	}

	private def writeTreeNodeData(out: OutputStream, treeNodeData: TreeNodeDataAccess) {
		import treeNodeData.ExtendedTreeNodeData

		streamJson(out) { jg =>
			jg.writeStartArray

			treeNodeData.foreach { node =>
				jg.writeStartObject

				jg.writeNumberField("id", node.id)
				for (parentId <- node.parentId) jg.writeNumberField("parentId", parentId)
				jg.writeStringField("label", node.label)
				jg.writeStringField("kind", node.kind.label)
				for (size <- node.size) jg.writeNumberField("size", size)
				for (traced <- node.traced) jg.writeBooleanField("traced", traced)
				for (sourceFileId <- node.sourceFileId) jg.writeNumberField("sourceFileId", sourceFileId)
				for (sourceLocationCount <- node.sourceLocationCount) jg.writeNumberField("sourceLocationCount", sourceLocationCount)
				for (methodStartLine <- node.methodStartLine) jg.writeNumberField("methodStartLine", methodStartLine)
				for (methodEndLine <- node.methodEndLine) jg.writeNumberField("methodEndLine", methodEndLine)
				for (isSurfaceMethod <- node.isSurfaceMethod) {
					if (node.isSurfaceMethod.isDefined) jg.writeBooleanField("isSurfaceMethod", isSurfaceMethod)
				}

				jg.writeEndObject
			}

			jg.writeEndArray
		}
	}

	private def writeMethodMappings(out: OutputStream, treeNodeData: TreeNodeDataAccess) {
		streamJson(out) { jg =>
			jg.writeStartArray

			treeNodeData.foreachMethodMapping { (methodSignatureNode) =>
				jg.writeStartObject
				jg.writeStringField("signature", methodSignatureNode.signature)
				jg.writeNumberField("node", methodSignatureNode.nodeId)
				jg.writeEndObject
			}

			jg.writeEndArray
		}
	}

	private def writeJspMappings(out: OutputStream, treeNodeData: TreeNodeDataAccess) {
		streamJson(out) { jg =>
			jg.writeStartArray

			treeNodeData.foreachJspMapping { (jspPath, nodeId) =>
				jg.writeStartObject
				jg.writeStringField("jsp", jspPath)
				jg.writeNumberField("node", nodeId)
				jg.writeEndObject
			}

			jg.writeEndArray
		}
	}

	private def writeRecordings(out: OutputStream, recordings: RecordingMetadataAccess) {
		streamJson(out) { jg =>
			jg.writeStartArray

			for (recording <- recordings.all) {
				jg.writeStartObject
				jg.writeNumberField("id", recording.id)
				jg.writeBooleanField("running", recording.running)
				for (label <- recording.clientLabel) jg.writeStringField("label", label)
				for (color <- recording.clientColor) jg.writeStringField("color", color)
				jg.writeEndObject
			}

			jg.writeEndArray
		}
	}

	private def writeSourceFiles(out: OutputStream, sourceDataAccess: SourceDataAccess): Unit = {
		streamJson(out) { jg =>
			jg.writeStartArray

			sourceDataAccess.foreachSourceFile { file =>
				jg.writeStartObject
				jg.writeNumberField("id", file.id)
				jg.writeStringField("path", file.path)
				jg.writeEndObject
			}

			jg.writeEndArray
		}
	}

	private def writeSourceLocations(out: OutputStream, sourceDataAccess: SourceDataAccess) = {
		streamJson(out) { jg =>
			jg.writeStartArray

			sourceDataAccess.foreachSourceLocation { location =>
				jg.writeStartObject
				jg.writeNumberField("id", location.id)
				jg.writeNumberField("sourceFileId", location.sourceFileId)
				jg.writeNumberField("startLine", location.startLine)
				jg.writeNumberField("endLine", location.endLine)
				for (startCharacter <- location.startCharacter) jg.writeNumberField("startCharacter", startCharacter)
				for (endCharacter <- location.endCharacter) jg.writeNumberField("endCharacter", endCharacter)
				jg.writeEndObject
			}

			jg.writeEndArray
		}
	}

	private def writeEncounters(out: OutputStream, recordings: RecordingMetadataAccess, encounters: TraceEncounterDataAccess) {
		streamJson(out) { jg =>
			jg.writeStartObject

			jg writeFieldName "all"
			jg.writeStartArray
			val allEncounters = encounters.getAllEncounters()
			allEncounters.foreach(encounter => {
				jg.writeStartObject()
				jg.writeNumberField("nodeId", encounter._1)
				for (sourceLocationId <- encounter._2) jg.writeNumberField("sourceLocationId", sourceLocationId)
				jg.writeEndObject()
			})
			jg.writeEndArray

			for (recording <- recordings.all.map(_.id)) {
				jg writeFieldName recording.toString
				jg.writeStartArray
				encounters.getRecordingEncounters(recording).foreach(recordingEncounter => {
					jg.writeStartObject()
					jg.writeNumberField("nodeId", recordingEncounter._1)
					for (sourceLocationId <- recordingEncounter._2) jg.writeNumberField("sourceLocationId", sourceLocationId)
					jg.writeEndObject()
				})
				jg.writeEndArray
			}

			jg.writeEndObject
		}
	}

	private def writeInputFile(out:OutputStream, input: String): Unit = {
		val file = new java.io.File(input)
		org.apache.commons.io.FileUtils.copyFile(file, out)
	}
}